/* Copyright 2012-2013 predic8 GmbH, www.predic8.com Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. */ package com.predic8.membrane.core.resolver; import java.io.File; import java.io.FileNotFoundException; import java.io.InputStream; import java.net.URI; import java.security.InvalidParameterException; import java.util.List; import com.predic8.membrane.annot.MCChildElement; import com.predic8.membrane.annot.MCElement; import com.predic8.membrane.core.cloud.etcd.*; import com.predic8.membrane.core.util.functionalInterfaces.Consumer; import org.w3c.dom.ls.LSInput; import org.w3c.dom.ls.LSResourceResolver; import com.google.common.base.Objects; import com.predic8.membrane.core.util.LSInputImpl; import com.predic8.xml.util.ExternalResolver; /** * A ResolverMap consists of a list of {@link SchemaResolver}s. * * It is itself a {@link Resolver}: Requests to resolve a URL are delegated * to the corresponding {@link SchemaResolver} child depending on the URL's * schema. * * Note that this class is not thread-safe! The ResolverMap is setup during * Membrane's single-threaded startup and is only used read-only thereafter. */ @MCElement(name="resolverMap") public class ResolverMap implements Cloneable, Resolver { private EtcdResolver etcdResolver; public static String combine(String... locations) { if (locations.length < 2) throw new InvalidParameterException(); if (locations.length > 2) { // lfold String[] l = new String[locations.length-1]; System.arraycopy(locations, 0, l, 0, locations.length-1); return combine(combine(l), locations[locations.length-1]); } String parent = locations[0]; String relativeChild = locations[1]; if (relativeChild.contains(":/") || relativeChild.contains(":\\") || parent == null || parent.length() == 0) return relativeChild; if (parent.startsWith("file://")) { if (relativeChild.startsWith("\\")) return "file://" + new File(relativeChild).getAbsolutePath(); //System.err.println(FileSchemaResolver.normalize(parent)); File parentFile = new File(FileSchemaResolver.normalize(parent)); //System.err.println(parentFile.getAbsolutePath()); if (!parent.endsWith("/") && !parent.endsWith("\\")) parentFile = parentFile.getParentFile(); //System.err.println(parentFile.getAbsolutePath()); String res = "file://" + new File(parentFile, relativeChild).getAbsolutePath(); if (relativeChild.endsWith("/") || relativeChild.endsWith("\\")) res += "/"; return res; } if (parent.contains(":/")) { try { return new URI(parent).resolve(relativeChild.replaceAll("\\\\", "/")).toString(); } catch (Exception e) { throw new RuntimeException(e); } } else if (parent.startsWith("/")) { try { return new URI("file:" + parent).resolve(relativeChild).toString().substring(5); } catch (Exception e) { throw new RuntimeException(e); } } else { File parentFile = new File(parent); if (!parent.endsWith("/") && !parent.endsWith("\\")) parentFile = parentFile.getParentFile(); return new File(parentFile, relativeChild).getAbsolutePath(); } } int count = 0; private String[] schemas; private SchemaResolver[] resolvers; public ResolverMap() { schemas = new String[10]; resolvers = new SchemaResolver[10]; // the default config addSchemaResolver(new ClasspathSchemaResolver()); addSchemaResolver(new HTTPSchemaResolver()); addSchemaResolver(new FileSchemaResolver()); } private ResolverMap(ResolverMap other) { count = other.count; schemas = new String[other.schemas.length]; resolvers = new SchemaResolver[other.resolvers.length]; System.arraycopy(other.schemas, 0, schemas, 0, count); System.arraycopy(other.resolvers, 0, resolvers, 0, count); } @Override public ResolverMap clone() { return new ResolverMap(this); } public void addSchemaResolver(SchemaResolver sr) { for (String schema : sr.getSchemas()) addSchemaResolver(schema == null ? null : schema + ":", sr); } private void addSchemaResolver(String schema, SchemaResolver resolver) { for (int i = 0; i < count; i++) if (Objects.equal(schemas[i], schema)) { // schema already known: replace resolver resolvers[i] = resolver; return; } // increase array size if (++count > schemas.length) { String[] schemas2 = new String[schemas.length * 2]; System.arraycopy(schemas, 0, schemas2, 0, schemas.length); schemas = schemas2; SchemaResolver[] resolvers2 = new SchemaResolver[resolvers.length * 2]; System.arraycopy(resolvers, 0, resolvers2, 0, resolvers.length); resolvers = resolvers2; } // determine target index int newIndex = count - 1; if (newIndex > 0 && schemas[newIndex - 1] == null) { // move 'null' resolver to last index schemas[newIndex] = schemas[newIndex - 1]; resolvers[newIndex] = resolvers[newIndex - 1]; newIndex--; } // insert resolver schemas[newIndex] = schema; resolvers[newIndex] = resolver; } private SchemaResolver getSchemaResolver(String uri) { for (int i = 0; i < count; i++) { if (schemas[i] == null) return resolvers[i]; if (uri.startsWith(schemas[i])) return resolvers[i]; } throw new RuntimeException("No SchemaResolver defined for " + uri); } public long getTimestamp(String uri) throws FileNotFoundException { return getSchemaResolver(uri).getTimestamp(uri); } public InputStream resolve(String uri) throws ResourceRetrievalException { try { return getSchemaResolver(uri).resolve(uri); } catch (ResourceRetrievalException e) { throw e; } } @Override public void observeChange(String uri, Consumer<InputStream> consumer) throws ResourceRetrievalException { try { getSchemaResolver(uri).observeChange(uri,consumer); } catch (ResourceRetrievalException e) { throw e; } } public List<String> getChildren(String uri) throws FileNotFoundException { return getSchemaResolver(uri).getChildren(uri); } public HTTPSchemaResolver getHTTPSchemaResolver() { return (HTTPSchemaResolver) getSchemaResolver("http:"); } public SchemaResolver getFileSchemaResolver() { return getSchemaResolver("file:"); } public LSResourceResolver toLSResourceResolver() { return new LSResourceResolver() { @Override public LSInput resolveResource(String type, String namespaceURI, String publicId, String systemId, String baseURI) { if (systemId == null) return null; try { if (!systemId.contains("://")) systemId = new URI(baseURI).resolve(systemId).toString(); return new LSInputImpl(publicId, systemId, resolve(systemId)); } catch (Exception e) { throw new RuntimeException(e); } } }; } public ExternalResolverConverter toExternalResolver() { return new ExternalResolverConverter(); } public EtcdResolver getEtcdResolver() { return etcdResolver; } @MCChildElement(order = 0) public void setEtcdResolver(EtcdResolver etcdResolver) { this.etcdResolver = etcdResolver; addSchemaResolver(etcdResolver); } public class ExternalResolverConverter { public ExternalResolver toExternalResolver() { return new ExternalResolver() { @Override public InputStream resolveAsFile(String filename, String baseDir) { try { if(baseDir != null) { return ResolverMap.this.resolve(combine(baseDir, filename)); } return ResolverMap.this.resolve(filename); } catch (Exception e) { throw new RuntimeException(e); } } @Override protected InputStream resolveViaHttp(Object url) { try { String url2 = (String) url; int q = url2.indexOf('?'); if (q == -1) url2 = url2.replaceAll("/[^/]+/\\.\\./", "/"); else url2 = url2.substring(0, q).replaceAll("/[^/]+/\\.\\./", "/") + url2.substring(q); return getSchemaResolver(url2).resolve(url2); } catch (ResourceRetrievalException e) { throw new RuntimeException(e); } } }; } } }